import java.io.File;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;
import java.nio.charset.StandardCharsets;
import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;

public class DotNet {
	private final Linker linker = Linker.nativeLinker();
	private final MethodHandle mh_load_assembly_and_get_function_pointer;

	public DotNet(String dotNetPath, String runtimeConfigPath) throws Throwable {
		var libDotNet = SymbolLookup.libraryLookup(dotNetPath + "/hostfxr.dll", Arena.global());

		// int (const wchar_t* runtime_config_path, const hostfxr_initialize_parameters* parameters,
		//      hostfxr_handle* host_context_handle);
		var mh_hostfxr_initialize_for_runtime_config = linker.downcallHandle(
				libDotNet.find("hostfxr_initialize_for_runtime_config").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS));

		// int (const hostfxr_handle host_context_handle, enum hostfxr_delegate_type type, void** delegate);
		var mh_hostfxr_get_runtime_delegate = linker.downcallHandle(
				libDotNet.find("hostfxr_get_runtime_delegate").orElseThrow(),
				FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT, ADDRESS));

		// int (const hostfxr_handle host_context_handle);
		var mh_hostfxr_close = linker.downcallHandle(
				libDotNet.find("hostfxr_close").orElseThrow(),
				FunctionDescriptor.ofVoid(ADDRESS));

		var hostHandle = MemorySegment.NULL;
		try (var arena = Arena.ofConfined()) {
			var outAddr = arena.allocate(ADDRESS, 1);
			int r = (int)mh_hostfxr_initialize_for_runtime_config.invokeExact(
					arena.allocateFrom(runtimeConfigPath, StandardCharsets.UTF_16LE),
					MemorySegment.NULL, outAddr);
			hostHandle = outAddr.get(ADDRESS, 0);
			if (r != 0 || hostHandle.equals(MemorySegment.NULL))
				throw new RuntimeException("hostfxr_initialize_for_runtime_config = " + r);
			r = (int)mh_hostfxr_get_runtime_delegate.invokeExact(hostHandle, 5, outAddr); // hdt_load_assembly_and_get_function_pointer=5
			var load_assembly_and_get_function_pointer = outAddr.get(ADDRESS, 0);
			if (r != 0 || load_assembly_and_get_function_pointer.equals(MemorySegment.NULL))
				throw new RuntimeException("hostfxr_get_runtime_delegate = " + r);

			// int (const wchar_t* assembly_path, const wchar_t* type_name, const wchar_t* method_name,
			//      const wchar_t* delegate_type_name, void* reserved, void** delegate);
			mh_load_assembly_and_get_function_pointer = linker.downcallHandle(load_assembly_and_get_function_pointer,
					FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS, ADDRESS, ADDRESS, ADDRESS));
		} finally {
			if (!hostHandle.equals(MemorySegment.NULL))
				mh_hostfxr_close.invokeExact(hostHandle);
		}
	}

	public MethodHandle loadFunction(String dllPath, String className, String methodName, FunctionDescriptor fd) {
		try (var arena = Arena.ofConfined()) {
			var outAddr = arena.allocate(ADDRESS, 1);
			int r = (int)mh_load_assembly_and_get_function_pointer.invokeExact(
					arena.allocateFrom(dllPath, StandardCharsets.UTF_16LE),
					arena.allocateFrom(className, StandardCharsets.UTF_16LE),
					arena.allocateFrom(methodName, StandardCharsets.UTF_16LE),
					MemorySegment.ofAddress(-1), MemorySegment.NULL, outAddr); // UNMANAGEDCALLERSONLY_METHOD=-1
			var funcAddr = outAddr.get(ADDRESS, 0);
			if (r != 0 || funcAddr.equals(MemorySegment.NULL))
				throw new RuntimeException("load_assembly_and_get_function_pointer = " + r);
			return linker.downcallHandle(funcAddr, fd);
		} catch (RuntimeException e) {
			throw e;
		} catch (Throwable e) {
			throw new RuntimeException(e);
		}
	}

	public static void main(String[] args) throws Throwable {
		var dotNet = new DotNet("C:/Program Files/dotnet/host/fxr/9.0.0",
				new File("cs/Hello.runtimeconfig.json").getCanonicalPath());
		var mhAdd = dotNet.loadFunction(new File("cs/Hello.exe").getCanonicalPath(),
				"Hello, Hello", "Add", FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT));
		System.out.println((int)mhAdd.invokeExact(1, 2));
		System.out.println("end");
	}
}
